/*! KeyTable 2.5.2
 * ©2009-2020 SpryMedia Ltd - datatables.net/license
 */

/**
 * @summary     KeyTable
 * @description Spreadsheet like keyboard navigation for DataTables
 * @version     2.5.2
 * @file        dataTables.keyTable.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     www.sprymedia.co.uk/contact
 * @copyright   Copyright 2009-2020 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net
 */

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'datatables.net'], function ($) {
            return factory($, window, document);
        });
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = function (root, $) {
            if (!root) {
                root = window;
            }

            if (!$ || !$.fn.dataTable) {
                $ = require('datatables.net')(root, $).$;
            }

            return factory($, root, root.document);
        };
    } else {
        // Browser
        factory(jQuery, window, document);
    }
}(function ($, window, document, undefined) {
    'use strict';
    var DataTable = $.fn.dataTable;
    var namespaceCounter = 0;


    var KeyTable = function (dt, opts) {
        // Sanity check that we are using DataTables 1.10 or newer
        if (!DataTable.versionCheck || !DataTable.versionCheck('1.10.8')) {
            throw 'KeyTable requires DataTables 1.10.8 or newer';
        }

        // User and defaults configuration object
        this.c = $.extend(true, {},
            DataTable.defaults.keyTable,
            KeyTable.defaults,
            opts
        );

        // Internal settings
        this.s = {
            /** @type {DataTable.Api} DataTables' API instance */
            dt: new DataTable.Api(dt),

            enable: true,

            /** @type {bool} Flag for if a draw is triggered by focus */
            focusDraw: false,

            /** @type {bool} Flag to indicate when waiting for a draw to happen.
             *   Will ignore key presses at this point
             */
            waitingForDraw: false,

            /** @type {object} Information about the last cell that was focused */
            lastFocus: null,

            /** @type {string} Unique namespace per instance */
            namespace: '.keyTable-' + (namespaceCounter++),

            /** @type {Node} Input element for tabbing into the table */
            tabInput: null
        };

        // DOM items
        this.dom = {};

        // Check if row reorder has already been initialised on this table
        var settings = this.s.dt.settings()[0];
        var exisiting = settings.keytable;
        if (exisiting) {
            return exisiting;
        }

        settings.keytable = this;
        this._constructor();
    };


    $.extend(KeyTable.prototype, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * API methods for DataTables API interface
         */

        /**
         * Blur the table's cell focus
         */
        blur: function () {
            this._blur();
        },

        /**
         * Enable cell focus for the table
         *
         * @param  {string} state Can be `true`, `false` or `-string navigation-only`
         */
        enable: function (state) {
            this.s.enable = state;
        },

        /**
         * Focus on a cell
         * @param  {integer} row    Row index
         * @param  {integer} column Column index
         */
        focus: function (row, column) {
            this._focus(this.s.dt.cell(row, column));
        },

        /**
         * Is the cell focused
         * @param  {object} cell Cell index to check
         * @returns {boolean} true if focused, false otherwise
         */
        focused: function (cell) {
            var lastFocus = this.s.lastFocus;

            if (!lastFocus) {
                return false;
            }

            var lastIdx = this.s.lastFocus.cell.index();
            return cell.row === lastIdx.row && cell.column === lastIdx.column;
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Constructor
         */

        /**
         * Initialise the KeyTable instance
         *
         * @private
         */
        _constructor: function () {
            this._tabInput();

            var that = this;
            var dt = this.s.dt;
            var table = $(dt.table().node());
            var namespace = this.s.namespace;
            var editorBlock = false;

            // Need to be able to calculate the cell positions relative to the table
            if (table.css('position') === 'static') {
                table.css('position', 'relative');
            }

            // Click to focus
            $(dt.table().body()).on('click' + namespace, 'th, td', function (e) {
                if (that.s.enable === false) {
                    return;
                }

                var cell = dt.cell(this);

                if (!cell.any()) {
                    return;
                }

                that._focus(cell, null, false, e);
            });

            // Key events
            $(document).on('keydown' + namespace, function (e) {
                if (!editorBlock) {
                    that._key(e);
                }
            });

            // Click blur
            if (this.c.blurable) {
                $(document).on('mousedown' + namespace, function (e) {
                    // Click on the search input will blur focus
                    if ($(e.target).parents('.dataTables_filter').length) {
                        that._blur();
                    }

                    // If the click was inside the DataTables container, don't blur
                    if ($(e.target).parents().filter(dt.table().container()).length) {
                        return;
                    }

                    // Don't blur in Editor form
                    if ($(e.target).parents('div.DTE').length) {
                        return;
                    }

                    // Or an Editor date input
                    if ($(e.target).parents('div.editor-datetime').length) {
                        return;
                    }

                    //If the click was inside the fixed columns container, don't blur
                    if ($(e.target).parents().filter('.DTFC_Cloned').length) {
                        return;
                    }

                    that._blur();
                });
            }

            if (this.c.editor) {
                var editor = this.c.editor;

                // Need to disable KeyTable when the main editor is shown
                editor.on('open.keyTableMain', function (e, mode, action) {
                    if (mode !== 'inline' && that.s.enable) {
                        that.enable(false);

                        editor.one('close' + namespace, function () {
                            that.enable(true);
                        });
                    }
                });

                if (this.c.editOnFocus) {
                    dt.on('key-focus' + namespace + ' key-refocus' + namespace, function (e, dt, cell, orig) {
                        that._editor(null, orig, true);
                    });
                }

                // Activate Editor when a key is pressed (will be ignored, if
                // already active).
                dt.on('key' + namespace, function (e, dt, key, cell, orig) {
                    that._editor(key, orig, false);
                });

                // Active editing on double click - it will already have focus from
                // the click event handler above
                $(dt.table().body()).on('dblclick' + namespace, 'th, td', function (e) {
                    if (that.s.enable === false) {
                        return;
                    }

                    var cell = dt.cell(this);

                    if (!cell.any()) {
                        return;
                    }

                    that._editor(null, e, true);
                });

                // While Editor is busy processing, we don't want to process any key events
                editor
                    .on('preSubmit', function () {
                        editorBlock = true;
                    })
                    .on('preSubmitCancelled', function () {
                        editorBlock = false;
                    })
                    .on('submitComplete', function () {
                        editorBlock = false;
                    });
            }

            // Stave saving
            if (dt.settings()[0].oFeatures.bStateSave) {
                dt.on('stateSaveParams' + namespace, function (e, s, d) {
                    d.keyTable = that.s.lastFocus ?
                        that.s.lastFocus.cell.index() :
                        null;
                });
            }

            dt.on('column-visibility' + namespace, function (e) {
                that._tabInput();
            });

            // Redraw - retain focus on the current cell
            dt.on('draw' + namespace, function (e) {
                that._tabInput();

                if (that.s.focusDraw) {
                    return;
                }

                var lastFocus = that.s.lastFocus;

                if (lastFocus && lastFocus.node && $(lastFocus.node).closest('body') === document.body) {
                    var relative = that.s.lastFocus.relative;
                    var info = dt.page.info();
                    var row = relative.row + info.start;

                    if (info.recordsDisplay === 0) {
                        return;
                    }

                    // Reverse if needed
                    if (row >= info.recordsDisplay) {
                        row = info.recordsDisplay - 1;
                    }

                    that._focus(row, relative.column, true, e);
                }
            });

            // Clipboard support
            if (this.c.clipboard) {
                this._clipboard();
            }

            dt.on('destroy' + namespace, function () {
                that._blur(true);

                // Event tidy up
                dt.off(namespace);

                $(dt.table().body())
                    .off('click' + namespace, 'th, td')
                    .off('dblclick' + namespace, 'th, td');

                $(document)
                    .off('mousedown' + namespace)
                    .off('keydown' + namespace)
                    .off('copy' + namespace)
                    .off('paste' + namespace);
            });

            // Initial focus comes from state or options
            var state = dt.state.loaded();

            if (state && state.keyTable) {
                // Wait until init is done
                dt.one('init', function () {
                    var cell = dt.cell(state.keyTable);

                    // Ensure that the saved cell still exists
                    if (cell.any()) {
                        cell.focus();
                    }
                });
            } else if (this.c.focus) {
                dt.cell(this.c.focus).focus();
            }
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Private methods
         */

        /**
         * Blur the control
         *
         * @param {boolean} [noEvents=false] Don't trigger updates / events (for destroying)
         * @private
         */
        _blur: function (noEvents) {
            if (!this.s.enable || !this.s.lastFocus) {
                return;
            }

            var cell = this.s.lastFocus.cell;

            $(cell.node()).removeClass(this.c.className);
            this.s.lastFocus = null;

            if (!noEvents) {
                this._updateFixedColumns(cell.index().column);

                this._emitEvent('key-blur', [this.s.dt, cell]);
            }
        },


        /**
         * Clipboard interaction handlers
         *
         * @private
         */
        _clipboard: function () {
            var dt = this.s.dt;
            var that = this;
            var namespace = this.s.namespace;

            // IE8 doesn't support getting selected text
            if (!window.getSelection) {
                return;
            }

            $(document).on('copy' + namespace, function (ejq) {
                var e = ejq.originalEvent;
                var selection = window.getSelection().toString();
                var focused = that.s.lastFocus;

                // Only copy cell text to clipboard if there is no other selection
                // and there is a focused cell
                if (!selection && focused) {
                    e.clipboardData.setData(
                        'text/plain',
                        focused.cell.render(that.c.clipboardOrthogonal)
                    );
                    e.preventDefault();
                }
            });

            $(document).on('paste' + namespace, function (ejq) {
                var e = ejq.originalEvent;
                var focused = that.s.lastFocus;
                var activeEl = document.activeElement;
                var editor = that.c.editor;
                var pastedText;

                if (focused && (!activeEl || activeEl.nodeName.toLowerCase() === 'body')) {
                    e.preventDefault();

                    if (window.clipboardData && window.clipboardData.getData) {
                        // IE
                        pastedText = window.clipboardData.getData('Text');
                    } else if (e.clipboardData && e.clipboardData.getData) {
                        // Everything else
                        pastedText = e.clipboardData.getData('text/plain');
                    }

                    if (editor) {
                        // Got Editor - need to activate inline editing,
                        // set the value and submit
                        editor
                            .inline(focused.cell.index())
                            .set(editor.displayed()[0], pastedText)
                            .submit();
                    } else {
                        // No editor, so just dump the data in
                        focused.cell.data(pastedText);
                        dt.draw(false);
                    }
                }
            });
        },


        /**
         * Get an array of the column indexes that KeyTable can operate on. This
         * is a merge of the user supplied columns and the visible columns.
         *
         * @private
         */
        _columns: function () {
            var dt = this.s.dt;
            var user = dt.columns(this.c.columns).indexes();
            var out = [];

            dt.columns(':visible').every(function (i) {
                if (user.indexOf(i) !== -1) {
                    out.push(i);
                }
            });

            return out;
        },


        /**
         * Perform excel like navigation for Editor by triggering an edit on key
         * press
         *
         * @param  {integer} key Key code for the pressed key
         * @param  {object} orig Original event
         * @private
         */
        _editor: function (key, orig, hardEdit) {
            // If nothing focused, we can't take any action
            if (!this.s.lastFocus) {
                return;
            }

            var that = this;
            var dt = this.s.dt;
            var editor = this.c.editor;
            var editCell = this.s.lastFocus.cell;
            var namespace = this.s.namespace;

            // Do nothing if there is already an inline edit in this cell
            if ($('div.DTE', editCell.node()).length) {
                return;
            }

            // Don't activate Editor on control key presses
            if (key !== null && (
                (key >= 0x00 && key <= 0x09) ||
                key === 0x0b ||
                key === 0x0c ||
                (key >= 0x0e && key <= 0x1f) ||
                (key >= 0x70 && key <= 0x7b) ||
                (key >= 0x7f && key <= 0x9f)
            )) {
                return;
            }

            orig.stopPropagation();

            // Return key should do nothing - for textareas it would empty the
            // contents
            if (key === 13) {
                orig.preventDefault();
            }

            var editInline = function () {
                editor
                    .one('open' + namespace, function () {
                        // Remove cancel open
                        editor.off('cancelOpen' + namespace);

                        // Excel style - select all text
                        if (!hardEdit) {
                            $('div.DTE_Field_InputControl input, div.DTE_Field_InputControl textarea').select();
                        }

                        // Reduce the keys the Keys listens for
                        dt.keys.enable(hardEdit ? 'tab-only' : 'navigation-only');

                        // On blur of the navigation submit
                        dt.on('key-blur.editor', function (e, dt, cell) {
                            if (editor.displayed() && cell.node() === editCell.node()) {
                                editor.submit();
                            }
                        });

                        // Highlight the cell a different colour on full edit
                        if (hardEdit) {
                            $(dt.table().container()).addClass('dtk-focus-alt');
                        }

                        // If the dev cancels the submit, we need to return focus
                        editor.on('preSubmitCancelled' + namespace, function () {
                            setTimeout(function () {
                                that._focus(editCell, null, false);
                            }, 50);
                        });

                        editor.on('submitUnsuccessful' + namespace, function () {
                            that._focus(editCell, null, false);
                        });

                        // Restore full key navigation on close
                        editor.one('close', function () {
                            dt.keys.enable(true);
                            dt.off('key-blur.editor');
                            editor.off(namespace);
                            $(dt.table().container()).removeClass('dtk-focus-alt');
                        });
                    })
                    .one('cancelOpen' + namespace, function () {
                        // `preOpen` can cancel the display of the form, so it
                        // might be that the open event handler isn't needed
                        editor.off(namespace);
                    })
                    .inline(editCell.index());
            };

            // Editor 1.7 listens for `return` on keyup, so if return is the trigger
            // key, we need to wait for `keyup` otherwise Editor would just submit
            // the content triggered by this keypress.
            if (key === 13) {
                hardEdit = true;

                $(document).one('keyup', function () { // immediately removed
                    editInline();
                });
            } else {
                editInline();
            }
        },


        /**
         * Emit an event on the DataTable for listeners
         *
         * @param  {string} name Event name
         * @param  {array} args Event arguments
         * @private
         */
        _emitEvent: function (name, args) {
            this.s.dt.iterator('table', function (ctx, i) {
                $(ctx.nTable).triggerHandler(name, args);
            });
        },


        /**
         * Focus on a particular cell, shifting the table's paging if required
         *
         * @param  {DataTables.Api|integer} row Can be given as an API instance that
         *   contains the cell to focus or as an integer. As the latter it is the
         *   visible row index (from the whole data set) - NOT the data index
         * @param  {integer} [column] Not required if a cell is given as the first
         *   parameter. Otherwise this is the column data index for the cell to
         *   focus on
         * @param {boolean} [shift=true] Should the viewport be moved to show cell
         * @private
         */
        _focus: function (row, column, shift, originalEvent) {
            var that = this;
            var dt = this.s.dt;
            var pageInfo = dt.page.info();
            var lastFocus = this.s.lastFocus;

            if (!originalEvent) {
                originalEvent = null;
            }

            if (!this.s.enable) {
                return;
            }

            if (typeof row !== 'number') {
                // Its an API instance - check that there is actually a row
                if (!row.any()) {
                    return;
                }

                // Convert the cell to a row and column
                var index = row.index();
                column = index.column;
                row = dt
                    .rows({filter: 'applied', order: 'applied'})
                    .indexes()
                    .indexOf(index.row);

                // Don't focus rows that were filtered out.
                if (row < 0) {
                    return;
                }

                // For server-side processing normalise the row by adding the start
                // point, since `rows().indexes()` includes only rows that are
                // available at the client-side
                if (pageInfo.serverSide) {
                    row += pageInfo.start;
                }
            }

            // Is the row on the current page? If not, we need to redraw to show the
            // page
            if (pageInfo.length !== -1 && (row < pageInfo.start || row >= pageInfo.start + pageInfo.length)) {
                this.s.focusDraw = true;
                this.s.waitingForDraw = true;

                dt
                    .one('draw', function () {
                        that.s.focusDraw = false;
                        that.s.waitingForDraw = false;
                        that._focus(row, column, undefined, originalEvent);
                    })
                    .page(Math.floor(row / pageInfo.length))
                    .draw(false);

                return;
            }

            // In the available columns?
            if ($.inArray(column, this._columns()) === -1) {
                return;
            }

            // De-normalise the server-side processing row, so we select the row
            // in its displayed position
            if (pageInfo.serverSide) {
                row -= pageInfo.start;
            }

            // Get the cell from the current position - ignoring any cells which might
            // not have been rendered (therefore can't use `:eq()` selector).
            var cells = dt.cells(null, column, {search: 'applied', order: 'applied'}).flatten();
            var cell = dt.cell(cells[row]);

            if (lastFocus) {
                // Don't trigger a refocus on the same cell
                if (lastFocus.node === cell.node()) {
                    this._emitEvent('key-refocus', [this.s.dt, cell, originalEvent || null]);
                    return;
                }

                // Otherwise blur the old focus
                this._blur();
            }

            // Clear focus from other tables
            this._removeOtherFocus();

            var node = $(cell.node());
            node.addClass(this.c.className);

            this._updateFixedColumns(column);

            // Shift viewpoint and page to make cell visible
            if (shift === undefined || shift === true) {
                this._scroll($(window), $(document.body), node, 'offset');

                var bodyParent = dt.table().body().parentNode;
                if (bodyParent !== dt.table().header().parentNode) {
                    var parent = $(bodyParent.parentNode);

                    this._scroll(parent, parent, node, 'position');
                }
            }

            // Event and finish
            this.s.lastFocus = {
                cell: cell,
                node: cell.node(),
                relative: {
                    row: dt.rows({page: 'current'}).indexes().indexOf(cell.index().row),
                    column: cell.index().column
                }
            };

            this._emitEvent('key-focus', [this.s.dt, cell, originalEvent || null]);
            dt.state.save();
        },


        /**
         * Handle key press
         *
         * @param  {object} e Event
         * @private
         */
        _key: function (e) {
            // If we are waiting for a draw to happen from another key event, then
            // do nothing for this new key press.
            if (this.s.waitingForDraw) {
                e.preventDefault();
                return;
            }

            var enable = this.s.enable;
            var navEnable = enable === true || enable === 'navigation-only';
            if (!enable) {
                return;
            }

            if ((e.keyCode === 0 || e.ctrlKey || e.metaKey || e.altKey) && !(e.ctrlKey && e.altKey)) {
                return;
            }

            // If not focused, then there is no key action to take
            var lastFocus = this.s.lastFocus;
            if (!lastFocus) {
                return;
            }

            // And the last focus still exists!
            if (!this.s.dt.cell(lastFocus.node).any()) {
                this.s.lastFocus = null;
                return;
            }

            var that = this;
            var dt = this.s.dt;
            var scrolling = this.s.dt.settings()[0].oScroll.sY ? true : false;

            // If we are not listening for this key, do nothing
            if (this.c.keys && $.inArray(e.keyCode, this.c.keys) === -1) {
                return;
            }

            switch (e.keyCode) {
                case 9: // tab
                    // `enable` can be tab-only
                    this._shift(e, e.shiftKey ? 'left' : 'right', true);
                    break;

                case 27: // esc
                    if (this.s.blurable && enable === true) {
                        this._blur();
                    }
                    break;

                case 33: // page up (previous page)
                case 34: // page down (next page)
                    if (navEnable && !scrolling) {
                        e.preventDefault();

                        dt
                            .page(e.keyCode === 33 ? 'previous' : 'next')
                            .draw(false);
                    }
                    break;

                case 35: // end (end of current page)
                case 36: // home (start of current page)
                    if (navEnable) {
                        e.preventDefault();
                        var indexes = dt.cells({page: 'current'}).indexes();
                        var colIndexes = this._columns();

                        this._focus(dt.cell(
                            indexes[e.keyCode === 35 ? indexes.length - 1 : colIndexes[0]]
                        ), null, true, e);
                    }
                    break;

                case 37: // left arrow
                    if (navEnable) {
                        this._shift(e, 'left');
                    }
                    break;

                case 38: // up arrow
                    if (navEnable) {
                        this._shift(e, 'up');
                    }
                    break;

                case 39: // right arrow
                    if (navEnable) {
                        this._shift(e, 'right');
                    }
                    break;

                case 40: // down arrow
                    if (navEnable) {
                        this._shift(e, 'down');
                    }
                    break;

                case 113: // F2 - Excel like hard edit
                    if (this.c.editor) {
                        this._editor(null, e, true);
                        break;
                    }
                // else fallthrough

                default:
                    // Everything else - pass through only when fully enabled
                    if (enable === true) {
                        this._emitEvent('key', [dt, e.keyCode, this.s.lastFocus.cell, e]);
                    }
                    break;
            }
        },

        /**
         * Remove focus from all tables other than this one
         */
        _removeOtherFocus: function () {
            var thisTable = this.s.dt.table().node();

            $.fn.dataTable.tables({api: true}).iterator('table', function (settings) {
                if (this.table().node() !== thisTable) {
                    this.cell.blur();
                }
            });
        },

        /**
         * Scroll a container to make a cell visible in it. This can be used for
         * both DataTables scrolling and native window scrolling.
         *
         * @param  {jQuery} container Scrolling container
         * @param  {jQuery} scroller  Item being scrolled
         * @param  {jQuery} cell      Cell in the scroller
         * @param  {string} posOff    `position` or `offset` - which to use for the
         *   calculation. `offset` for the document, otherwise `position`
         * @private
         */
        _scroll: function (container, scroller, cell, posOff) {
            var offset = cell[posOff]();
            var height = cell.outerHeight();
            var width = cell.outerWidth();

            var scrollTop = scroller.scrollTop();
            var scrollLeft = scroller.scrollLeft();
            var containerHeight = container.height();
            var containerWidth = container.width();

            // If Scroller is being used, the table can be `position: absolute` and that
            // needs to be taken account of in the offset. If no Scroller, this will be 0
            if (posOff === 'position') {
                offset.top += parseInt(cell.closest('table').css('top'), 10);
            }

            // Top correction
            if (offset.top < scrollTop) {
                scroller.scrollTop(offset.top);
            }

            // Left correction
            if (offset.left < scrollLeft) {
                scroller.scrollLeft(offset.left);
            }

            // Bottom correction
            if (offset.top + height > scrollTop + containerHeight && height < containerHeight) {
                scroller.scrollTop(offset.top + height - containerHeight);
            }

            // Right correction
            if (offset.left + width > scrollLeft + containerWidth && width < containerWidth) {
                scroller.scrollLeft(offset.left + width - containerWidth);
            }
        },


        /**
         * Calculate a single offset movement in the table - up, down, left and
         * right and then perform the focus if possible
         *
         * @param  {object}  e           Event object
         * @param  {string}  direction   Movement direction
         * @param  {boolean} keyBlurable `true` if the key press can result in the
         *   table being blurred. This is so arrow keys won't blur the table, but
         *   tab will.
         * @private
         */
        _shift: function (e, direction, keyBlurable) {
            var that = this;
            var dt = this.s.dt;
            var pageInfo = dt.page.info();
            var rows = pageInfo.recordsDisplay;
            var currentCell = this.s.lastFocus.cell;
            var columns = this._columns();

            if (!currentCell) {
                return;
            }

            var currRow = dt
                .rows({filter: 'applied', order: 'applied'})
                .indexes()
                .indexOf(currentCell.index().row);

            // When server-side processing, `rows().indexes()` only gives the rows
            // that are available at the client-side, so we need to normalise the
            // row's current position by the display start point
            if (pageInfo.serverSide) {
                currRow += pageInfo.start;
            }

            var currCol = dt
                .columns(columns)
                .indexes()
                .indexOf(currentCell.index().column);

            var
                row = currRow,
                column = columns[currCol]; // row is the display, column is an index

            if (direction === 'right') {
                if (currCol >= columns.length - 1) {
                    row++;
                    column = columns[0];
                } else {
                    column = columns[currCol + 1];
                }
            } else if (direction === 'left') {
                if (currCol === 0) {
                    row--;
                    column = columns[columns.length - 1];
                } else {
                    column = columns[currCol - 1];
                }
            } else if (direction === 'up') {
                row--;
            } else if (direction === 'down') {
                row++;
            }

            if (row >= 0 && row < rows && $.inArray(column, columns) !== -1) {
                if (e) {
                    e.preventDefault();
                }

                this._focus(row, column, true, e);
            } else if (!keyBlurable || !this.c.blurable) {
                // No new focus, but if the table isn't blurable, then don't loose
                // focus
                if (e) {
                    e.preventDefault();
                }
            } else {
                this._blur();
            }
        },


        /**
         * Create and insert a hidden input element that can receive focus on behalf
         * of the table
         *
         * @private
         */
        _tabInput: function () {
            var that = this;
            var dt = this.s.dt;
            var tabIndex = this.c.tabIndex !== null ?
                this.c.tabIndex :
                dt.settings()[0].iTabIndex;

            if (tabIndex == -1) {
                return;
            }

            // Only create the input element once on first class
            if (!this.s.tabInput) {
                var div = $('<div><input type="text" tabindex="' + tabIndex + '"/></div>')
                    .css({
                        position: 'absolute',
                        height: 1,
                        width: 0,
                        overflow: 'hidden'
                    });

                div.children().on('focus', function (e) {
                    var cell = dt.cell(':eq(0)', that._columns(), {page: 'current'});

                    if (cell.any()) {
                        that._focus(cell, null, true, e);
                    }
                });

                this.s.tabInput = div;
            }

            // Insert the input element into the first cell in the table's body
            var cell = this.s.dt.cell(':eq(0)', '0:visible', {page: 'current', order: 'current'}).node();
            if (cell) {
                $(cell).prepend(this.s.tabInput);
            }
        },

        /**
         * Update fixed columns if they are enabled and if the cell we are
         * focusing is inside a fixed column
         * @param  {integer} column Index of the column being changed
         * @private
         */
        _updateFixedColumns: function (column) {
            var dt = this.s.dt;
            var settings = dt.settings()[0];

            if (settings._oFixedColumns) {
                var leftCols = settings._oFixedColumns.s.iLeftColumns;
                var rightCols = settings.aoColumns.length - settings._oFixedColumns.s.iRightColumns;

                if (column < leftCols || column >= rightCols) {
                    dt.fixedColumns().update();
                }
            }
        }
    });


    /**
     * KeyTable default settings for initialisation
     *
     * @namespace
     * @name KeyTable.defaults
     * @static
     */
    KeyTable.defaults = {
        /**
         * Can focus be removed from the table
         * @type {Boolean}
         */
        blurable: true,

        /**
         * Class to give to the focused cell
         * @type {String}
         */
        className: 'focus',

        /**
         * Enable or disable clipboard support
         * @type {Boolean}
         */
        clipboard: true,

        /**
         * Orthogonal data that should be copied to clipboard
         * @type {string}
         */
        clipboardOrthogonal: 'display',

        /**
         * Columns that can be focused. This is automatically merged with the
         * visible columns as only visible columns can gain focus.
         * @type {String}
         */
        columns: '', // all

        /**
         * Editor instance to automatically perform Excel like navigation
         * @type {Editor}
         */
        editor: null,

        /**
         * Trigger editing immediately on focus
         * @type {boolean}
         */
        editOnFocus: false,

        /**
         * Select a cell to automatically select on start up. `null` for no
         * automatic selection
         * @type {cell-selector}
         */
        focus: null,

        /**
         * Array of keys to listen for
         * @type {null|array}
         */
        keys: null,

        /**
         * Tab index for where the table should sit in the document's tab flow
         * @type {integer|null}
         */
        tabIndex: null
    };


    KeyTable.version = "2.5.2";


    $.fn.dataTable.KeyTable = KeyTable;
    $.fn.DataTable.KeyTable = KeyTable;


    DataTable.Api.register('cell.blur()', function () {
        return this.iterator('table', function (ctx) {
            if (ctx.keytable) {
                ctx.keytable.blur();
            }
        });
    });

    DataTable.Api.register('cell().focus()', function () {
        return this.iterator('cell', function (ctx, row, column) {
            if (ctx.keytable) {
                ctx.keytable.focus(row, column);
            }
        });
    });

    DataTable.Api.register('keys.disable()', function () {
        return this.iterator('table', function (ctx) {
            if (ctx.keytable) {
                ctx.keytable.enable(false);
            }
        });
    });

    DataTable.Api.register('keys.enable()', function (opts) {
        return this.iterator('table', function (ctx) {
            if (ctx.keytable) {
                ctx.keytable.enable(opts === undefined ? true : opts);
            }
        });
    });

    DataTable.Api.register('keys.move()', function (dir) {
        return this.iterator('table', function (ctx) {
            if (ctx.keytable) {
                ctx.keytable._shift(null, dir, false);
            }
        });
    });

// Cell selector
    DataTable.ext.selector.cell.push(function (settings, opts, cells) {
        var focused = opts.focused;
        var kt = settings.keytable;
        var out = [];

        if (!kt || focused === undefined) {
            return cells;
        }

        for (var i = 0, ien = cells.length; i < ien; i++) {
            if ((focused === true && kt.focused(cells[i])) ||
                (focused === false && !kt.focused(cells[i]))
            ) {
                out.push(cells[i]);
            }
        }

        return out;
    });


// Attach a listener to the document which listens for DataTables initialisation
// events so we can automatically initialise
    $(document).on('preInit.dt.dtk', function (e, settings, json) {
        if (e.namespace !== 'dt') {
            return;
        }

        var init = settings.oInit.keys;
        var defaults = DataTable.defaults.keys;

        if (init || defaults) {
            var opts = $.extend({}, defaults, init);

            if (init !== false) {
                new KeyTable(settings, opts);
            }
        }
    });


    return KeyTable;
}));
